/*
 * Copyright (C) 2008 ZXing authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.zxing.journeyapps.barcodescanner.camera;

import com.google.zxing.client.ohos.AmbientLightManager;
import com.google.zxing.client.ohos.camera.CameraConfigurationUtils;
import com.google.zxing.client.ohos.camera.open.OpenCameraInterface;
import com.google.zxing.journeyapps.barcodescanner.BusHelper;
import com.google.zxing.journeyapps.barcodescanner.Size;
import com.google.zxing.journeyapps.barcodescanner.SourceData;
import ohos.agp.graphics.Surface;
import ohos.agp.utils.Rect;
import ohos.app.Context;
import ohos.app.Environment;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.media.camera.CameraKit;
import ohos.media.camera.device.Camera;
import ohos.media.camera.device.CameraConfig;
import ohos.media.camera.device.CameraInfo;
import ohos.media.camera.device.FrameConfig;
import ohos.media.camera.device.CameraStateCallback;
import ohos.media.camera.device.CameraAbility;
import ohos.media.camera.params.Metadata;
import ohos.media.camera.params.PropertyKey;
import ohos.media.image.Image;
import ohos.media.image.ImageReceiver;
import ohos.media.image.common.ImageFormat;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Wrapper to manage the Camera. This is not thread-safe, and the methods must always be called
 * from the same thread.
 *
 *
 * Call order:
 *
 * 1. setCameraSettings()
 * 2. open(), set desired preview size (any order)
 * 3. configure(), setPreviewDisplay(holder) (any order)
 * 4. startPreview()
 * 5. requestPreviewFrame (repeat)
 * 6. stopPreview()
 * 7. close()
 */
public final class CameraManager {

    private static final String TAG = CameraManager.class.getSimpleName();

    private static final int SCREEN_WIDTH = 2340;
    private static final int SCREEN_HEIGHT = 1080;
    private static final int SCREEN_WIDTH_FRONT = 320;
    private static final int SCREEN_HEIGHT_FRONT = 240;

    private Camera mCamera;
    private CameraInfo mCameraInfo;
    private CameraConfig.Builder mCameraConfigBuilder;
    private FrameConfig.Builder previewFrameConfigBuilder;
    private ImageReceiver mImageReceiver;

    private AutoFocusManager autoFocusManager;
    private AmbientLightManager ambientLightManager;

    private boolean previewing;
    private String defaultParameters;

    // User parameters
    private CameraSettings settings = new CameraSettings();

    private DisplayConfiguration displayConfiguration;

    // Actual chosen preview size
    private Size requestedPreviewSize;
    private Size previewSize;

    private int rotationDegrees = -1;    // camera rotation vs display rotation

    private Context context;
    private EventHandler eventHandler;
    private CameraSurface mCameraSurface;

    private void saveImage(byte[] bitmapPixels) {
        FileOutputStream output = null;
        try {
            String fileName = "IMG_" + UUID.randomUUID() + ".jpg";
            File targetFile = new File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), fileName);
            output = new FileOutputStream(targetFile);
            output.write(bitmapPixels);
        } catch (IOException e) {
        } finally {
            try {
                if (output != null) {
                    output.close();
                }
            } catch (IOException e) {
            }
        }
    }

    private final class CameraPreviewCallback implements ImageReceiver.IImageArrivalListener {
        private PreviewCallback callback;

        private Size resolution;

        public CameraPreviewCallback() {
        }

        public void setResolution(Size resolution) {
            this.resolution = resolution;
        }

        public void setCallback(PreviewCallback callback) {
            this.callback = callback;
        }

        private byte[] translateImageToJpeg(Image image) {
            Image.Component component = image.getComponent(ImageFormat.ComponentType.JPEG);
            byte[] data = new byte[component.remaining()];
            ByteBuffer buffer = component.getBuffer();
            buffer.get(data);
            return data;
        }

        @Override
        public void onImageArrival(ImageReceiver imageReceiver) {
            Size cameraResolution = resolution;
            PreviewCallback callback = this.callback;
            if (cameraResolution != null && callback != null) {
                try {
                    Image image = imageReceiver.readLatestImage();
                    if (image == null)
                        return;

                    byte[] data = translateImageToJpeg(image);
                    image.release();
                    SourceData source = new SourceData(data, cameraResolution.width, cameraResolution.height, ImageFormat.JPEG, getCameraRotation());

                    if (mCameraInfo.getFacingType() == CameraInfo.FacingType.CAMERA_FACING_FRONT) {
                        source.setPreviewMirrored(true);
                    }
                    callback.onPreview(source);
                } catch (RuntimeException e) {
                    // Could be:
                    // java.lang.RuntimeException: getParameters failed (empty parameters)
                    // IllegalArgumentException: Image data does not match the resolution
                    Logger.getLogger(TAG).log(Level.SEVERE, "Camera preview failed", e);
                    callback.onPreviewError(e);
                }
            } else {
                Logger.getLogger(TAG).config("Got preview callback, but no handler or resolution available");
                if (callback != null) {
                    // Should generally not happen
                    callback.onPreviewError(new Exception("No resolution available"));
                }
            }
        }
    }

    private CameraStateCallback cameraStateCallback = new CameraStateCallback() {
        @Override
        public void onCreated(Camera camera) {
            super.onCreated(camera);
            CameraManager.this.mCamera = camera;
            mCameraInfo = CameraKit.getInstance(context).getCameraInfo(camera.getCameraId());
            BusHelper.getINSTANCE().sendAction("cameraInitCompleted",null);
            BusHelper.getINSTANCE().sendAction("cameraInitCompleted2",null);
        }

        @Override
        public void onCreateFailed(String cameraId, int errorCode) {
            super.onCreateFailed(cameraId, errorCode);
            Logger.getLogger(TAG).severe("Failed to open camera error code " + errorCode);
        }

        @Override
        public void onConfigured(Camera camera) {
            super.onConfigured(camera);
            Logger.getLogger(TAG).severe("CameraStateCallback onConfigured");
            startPreview(mCameraSurface);
            BusHelper.getINSTANCE().sendAction("CameraPreviewStarted",null);
        }

        @Override
        public void onPartialConfigured(Camera camera) {
            super.onPartialConfigured(camera);
        }

        @Override
        public void onConfigureFailed(Camera camera, int errorCode) {
            super.onConfigureFailed(camera, errorCode);
        }

        @Override
        public void onReleased(Camera camera) {
            super.onReleased(camera);
        }

        @Override
        public void onFatalError(Camera camera, int errorCode) {
            super.onFatalError(camera, errorCode);
        }

        @Override
        public void onCaptureRun(Camera camera) {
            super.onCaptureRun(camera);
        }

        @Override
        public void onCaptureIdle(Camera camera) {
            super.onCaptureIdle(camera);
        }
    };

    /**
     * Preview frames are delivered here, which we pass on to the registered handler. Make sure to
     * clear the handler so it will only receive one message.
     */
    private final CameraPreviewCallback cameraPreviewCallback;

    public CameraManager(Context context) {
        this.context = context;
        cameraPreviewCallback = new CameraPreviewCallback();
    }

    /**
     * Must be called from camera thread.
     */
    public void open() {
        Logger.getLogger(TAG).warning("start open");
        eventHandler = new EventHandler(EventRunner.getMainEventRunner());

        OpenCameraInterface.open(CameraKit.getInstance(context),settings.getRequestedCameraId(),cameraStateCallback,eventHandler);
//        if (camera == null) {
//            throw new RuntimeException("Failed to open camera");
//        }

//        int cameraId = OpenCameraInterface.getCameraId(settings.getRequestedCameraId());
//        cameraInfo = new Camera.CameraInfo();
//        Camera.getCameraInfo(cameraId, cameraInfo);
    }

    /**
     * Configure the camera parameters, including preview size.
     *
     * The camera must be opened before calling this.
     *
     * Must be called from camera thread.
     */
    public void configure() {
        if (mCamera == null) {
            throw new RuntimeException("Camera not open");
        }
        mCameraConfigBuilder = mCamera.getCameraConfigBuilder();
        List<ohos.media.image.common.Size> supportedSizes = CameraKit.getInstance(context)
                .getCameraAbility(mCamera.getCameraId())
                .getSupportedSizes(ImageFormat.JPEG);
        mImageReceiver = ImageReceiver.create(SCREEN_WIDTH, SCREEN_HEIGHT, ImageFormat.JPEG, 3);
        mImageReceiver.setImageArrivalListener(cameraPreviewCallback);
        mCameraConfigBuilder.addSurface(mImageReceiver.getRecevingSurface());

        //setParameters(supportedSizes.get(0));
        setParameters(new ohos.media.image.common.Size(SCREEN_WIDTH_FRONT, SCREEN_HEIGHT_FRONT));
    }

    private FrameConfig.Builder getPreviewFrameConfigBuilder() {
        if (previewFrameConfigBuilder == null) {
            previewFrameConfigBuilder = mCamera.getFrameConfigBuilder(Camera.FrameConfigType.FRAME_CONFIG_PREVIEW);
        }
        return previewFrameConfigBuilder;
    }

    /**
     * Must be called from camera thread.
     */
    public void setPreviewDisplay(Surface surface) throws IOException {
        mCameraSurface = new CameraSurface(surface);
        setPreviewDisplay(mCameraSurface);
    }

    public void setPreviewDisplay(CameraSurface surface) throws IOException {
        mCameraSurface = surface;
        surface.setPreview(mCameraConfigBuilder);
        mCamera.configure(mCameraConfigBuilder.build());
    }

    /**
     * Asks the camera hardware to begin drawing preview frames to the screen.
     *
     * Must be called from camera thread.
     * @param surface
     */
    public void startPreview(CameraSurface surface) {
        Camera theCamera = mCamera;
        if (theCamera != null && !previewing) {
            getPreviewFrameConfigBuilder();
            previewFrameConfigBuilder.addSurface(surface.getSurfaceTexture());

            theCamera.triggerLoopingCapture(previewFrameConfigBuilder.build());
            Logger.getLogger(TAG).warning("startPreview triggerLoopingCapture");

            previewing = true;
            autoFocusManager = new AutoFocusManager(mCamera, settings);
            ambientLightManager = new AmbientLightManager(context, this, settings);
            ambientLightManager.start();
        }
    }

    /**
     * Tells the camera to stop drawing preview frames.
     *
     * Must be called from camera thread.
     */
    public void stopPreview() {
        if (autoFocusManager != null) {
            autoFocusManager.stop();
            autoFocusManager = null;
        }
        if (ambientLightManager != null) {
            ambientLightManager.stop();
            ambientLightManager = null;
        }
        if (mCamera != null && previewing) {
            mCamera.stopLoopingCapture();
            cameraPreviewCallback.setCallback(null);
            previewing = false;
        }
    }

    /**
     * Closes the camera driver if still in use.
     *
     * Must be called from camera thread.
     */
    public void close() {
        if (mCamera != null) {
            mCamera.release();
            mCamera = null;
        }
    }

    /**
     * @return true if the camera rotation is perpendicular to the current display rotation.
     */
    public boolean isCameraRotated() {
        if (rotationDegrees == -1) {
            throw new IllegalStateException("Rotation not calculated yet. Call configure() first.");
        }
        return rotationDegrees % 180 != 0;
    }

    /**
     *
     * @return the camera rotation relative to display rotation, in degrees. Typically 0 if the
     *    display is in landscape orientation.
     */
    public int getCameraRotation() {
        return rotationDegrees;
    }

    private List<PropertyKey.Key<?>> getDefaultCameraParameters() {
        CameraAbility cameraAbility = CameraKit.getInstance(context).getCameraAbility(mCamera.getCameraId());
        List<PropertyKey.Key<?>> supportedParameters = cameraAbility.getSupportedProperties();
        if (defaultParameters == null) {
            StringBuilder stringBuilder = new StringBuilder();
            for (PropertyKey.Key<?> key:supportedParameters) {
                stringBuilder.append(key.getName()).append("=").append(cameraAbility.getPropertyValue(key)).append(";");
            }
            defaultParameters = stringBuilder.toString();
        }

        return supportedParameters;
    }

    private void setDesiredParameters(boolean safeMode) {
        List<PropertyKey.Key<?>> parameters = getDefaultCameraParameters();

        //noinspection ConstantConditions
        if (parameters == null) {
            Logger.getLogger(TAG).warning("Device error: no camera parameters are available. Proceeding without configuration.");
            return;
        }

        if (safeMode) {
            Logger.getLogger(TAG).warning("In camera config safe mode -- most settings will not be honored");
        }

        getPreviewFrameConfigBuilder();
        CameraAbility cameraAbility = CameraKit.getInstance(context).getCameraAbility(mCamera.getCameraId());
        CameraConfigurationUtils.setFocus(cameraAbility, previewFrameConfigBuilder,settings.getFocusMode(), safeMode);

        if (!safeMode) {
            CameraConfigurationUtils.setTorch(cameraAbility,previewFrameConfigBuilder, false);

            // todo 无设置图片反色的方法，舍弃
            /*if (settings.isScanInverted()) {
                CameraConfigurationUtils.setInvertColor(parameters);
            }*/

            // TODO 设置场景模式为扫码
            if (settings.isBarcodeSceneModeEnabled()) {
                // CameraConfigurationUtils.setBarcodeSceneMode(frameConfigBuilder);
            }

            if (settings.isMeteringEnabled()) {

            }

        }

        List<Size> previewSizes = getPreviewSizes(cameraAbility,previewFrameConfigBuilder);
        if (previewSizes.size() == 0) {
            requestedPreviewSize = null;
        } else {
            requestedPreviewSize = displayConfiguration.getBestPreviewSize(previewSizes, isCameraRotated());
        }
    }

    private static List<Size> getPreviewSizes(CameraAbility parameters, FrameConfig.Builder frameConfigBuilder) {
        List<ohos.media.image.common.Size> rawSupportedSizes = parameters.getSupportedSizes(ImageFormat.JPEG);
        List<Size> previewSizes = new ArrayList<>();
        if (rawSupportedSizes == null) {
            Rect defaultSize = frameConfigBuilder.getAfRect();
            if (defaultSize != null) {
                Size previewSize = new Size(defaultSize.getWidth(), defaultSize.getHeight());
                previewSizes.add(previewSize);
            }
            return previewSizes;
        }
        for (ohos.media.image.common.Size size : rawSupportedSizes) {
            previewSizes.add(new Size(size.width, size.height));
        }
        return previewSizes;
    }

    private int calculateDisplayRotation() {
        int rotation = displayConfiguration.getRotation();
        /*int degrees = 0;
        switch (rotation) {
            case Surface.ROTATION_0:
                degrees = 0;
                break;
            case Surface.ROTATION_90:
                degrees = 90;
                break;
            case Surface.ROTATION_180:
                degrees = 180;
                break;
            case Surface.ROTATION_270:
                degrees = 270;
                break;
        }*/

        /*int result;
        if (cameraInfo.getFacingType() == CameraInfo.FacingType.CAMERA_FACING_FRONT) {
            result = (cameraInfo.orientation + degrees) % 360;
            result = (360 - result) % 360;  // compensate the mirror
        } else {  // back-facing
            result = (cameraInfo.orientation - degrees + 360) % 360;
        }
        Logger.getLogger(TAG).info("Camera Display Orientation: " + result);
        return result;*/

        return 0;
    }

    private void setCameraDisplayOrientation(int rotation) {
        getPreviewFrameConfigBuilder().setImageRotation(rotation);
//        camera.setDisplayOrientation(rotation);
    }

    private void setParameters(ohos.media.image.common.Size size) {
        Logger.getLogger(TAG).warning("setParameters");
        try {
            this.rotationDegrees = calculateDisplayRotation();
            Logger.getLogger(TAG).warning("setParameters rotationDegrees " + rotationDegrees);
            setCameraDisplayOrientation(rotationDegrees);
        } catch (Exception e) {
            Logger.getLogger(TAG).warning( "Failed to set rotation.");
            e.printStackTrace();
        }
        try {
            setDesiredParameters(false);
        } catch (Exception e) {
            // Failed, use safe mode
            try {
                setDesiredParameters(true);
            } catch (Exception e2) {
                // Well, darn. Give up
                Logger.getLogger(TAG).warning("Camera rejected even safe-mode parameters! No configuration");
            }
        }

//        Camera.Size realPreviewSize = camera.getParameters().getPreviewSize();
//        ParameterKey.Key<Rect> cropRegionKey = new ParameterKey.Key<>("ohos.camera.cropRegion", Rect.class);
//        Rect rect = mCamera.getFrameConfigBuilder(ImageFormat.YUV420_888).get(cropRegionKey);
        if (size == null) {
            previewSize = requestedPreviewSize;
        } else {
            previewSize = new Size(size.width, size.height);
        }
        cameraPreviewCallback.setResolution(previewSize);
    }

    /**
     * This returns false if the camera is not opened yet, failed to open, or has
     * been closed.
     */
    public boolean isOpen() {
        return mCamera != null;
    }

    /**
     * Actual preview size in *natural camera* orientation. null if not determined yet.
     *
     * @return preview size
     */
    public Size getNaturalPreviewSize() {
        return previewSize;
    }

    /**
     * Actual preview size in *current display* rotation. null if not determined yet.
     *
     * @return preview size
     */
    public Size getPreviewSize() {
        if (previewSize == null) {
            return null;
        } else if (this.isCameraRotated()) {
            return previewSize.rotate();
        } else {
            return previewSize;
        }
    }

    /**
     * A single preview frame will be returned to the supplied callback.
     *
     * The thread on which this called is undefined, so a Handler should be used to post the result
     * to the correct thread.
     *
     * @param callback The callback to receive the preview.
     */
    public void requestPreviewFrame(PreviewCallback callback) {
        Camera theCamera = mCamera;
        if (theCamera != null && previewing) {
            cameraPreviewCallback.setCallback(callback);

            getPreviewFrameConfigBuilder();
            /*List<ohos.media.image.common.Size> supportedSizes = CameraKit.getInstance(context)
                    .getCameraAbility(mCamera.getCameraId())
                    .getSupportedSizes(ImageFormat.YUV420_888);

            ImageReceiver imageReceiver = ImageReceiver.create(supportedSizes.get(0).width, supportedSizes.get(0).height, ImageFormat.YUV420_888, 3);*/
            mImageReceiver.setImageArrivalListener(cameraPreviewCallback);
            previewFrameConfigBuilder.addSurface(mImageReceiver.getRecevingSurface());
            mCamera.triggerLoopingCapture(previewFrameConfigBuilder.build());
        }
    }



    public CameraSettings getCameraSettings() {
        return settings;
    }

    public void setCameraSettings(CameraSettings settings) {
        this.settings = settings;
    }

    public DisplayConfiguration getDisplayConfiguration() {
        return displayConfiguration;
    }

    public void setDisplayConfiguration(DisplayConfiguration displayConfiguration) {
        this.displayConfiguration = displayConfiguration;
    }

    public void setTorch(boolean on) {
        if (mCamera != null) {
            try {
                boolean isOn = isTorchOn();
                if (on != isOn) {
                    if (autoFocusManager != null) {
                        autoFocusManager.stop();
                    }
                    FrameConfig.Builder frameConfigBuilder = getPreviewFrameConfigBuilder();
                    CameraAbility cameraAbility = CameraKit.getInstance(context).getCameraAbility(mCamera.getCameraId());
                    CameraConfigurationUtils.setTorch(cameraAbility,frameConfigBuilder, on);
                    if (settings.isExposureEnabled()) {
                        CameraConfigurationUtils.setBestExposure(frameConfigBuilder, on);
                    }
                    mCamera.flushCaptures();
                    mCamera.triggerLoopingCapture(frameConfigBuilder.build());

                    if (autoFocusManager != null) {
                        autoFocusManager.start();
                    }
                }
            } catch (RuntimeException e) {
                // Camera error. Could happen if the camera is being closed.
                Logger.getLogger(TAG).log(Level.SEVERE, "Failed to set torch", e);
            }
        }
    }

    /**
     * Changes the settings for Camera.
     *
     * @param callback {@link CameraParametersCallback}
     */
    public void changeCameraParameters(CameraParametersCallback callback) {
        if (mCamera != null) {
            try {
//                camera.configure();
//                camera.setParameters(callback.changeCameraParameters(camera.getParameters()));
            } catch (RuntimeException e) {
                // Camera error. Could happen if the camera is being closed.
                Logger.getLogger(TAG).log(Level.SEVERE, "Failed to change camera parameters", e);
            }
        }
    }

    /**
     *
     * @return true if the torch is on
     * @throws RuntimeException if there is a camera error
     */
    public boolean isTorchOn() {
        FrameConfig.Builder configBuilder = mCamera.getFrameConfigBuilder(Camera.FrameConfigType.FRAME_CONFIG_PREVIEW);
        if (configBuilder != null) {
            int flashMode = configBuilder.getFlashMode();
            return flashMode == Metadata.FlashMode.FLASH_ALWAYS_OPEN;
        } else {
            return false;
        }
    }

    /**
     * Returns the Camera. This returns null if the camera is not opened yet, failed to open, or has
     * been closed.
     *
     * @return the Camera
     */
    public Camera getCamera() {
        return mCamera;
    }
}
